#include <stdlib.h>
#include <stdio.h>
#include <alsa/asoundlib.h>
#include "output.h"

typedef struct alsa_output
{
   output_status status;
   buffer_available_proc callback;
   void* user_data;
   snd_pcm_t* dev;
   snd_async_handler_t* pcm_callback;
   snd_pcm_uframes_t buffer_size;
   snd_pcm_uframes_t period_size;
} alsa_output;

#define check_alsa_error(err)       { if (err < 0) { printf("%s(%d): ALSA error #%d (%s)\n", __FILE__, __LINE__, err, snd_strerror(err)); return e_internal_error; } }
#define check_alsa_error_void(err)  { if (err < 0) { printf("%s(%d): ALSA error #%d (%s)\n", __FILE__, __LINE__, err, snd_strerror(err)); return; } }

int output_init(output_ctx** pctx, const char* device)
{
   if (pctx == NULL)
      return e_invalid_argument;

   if (device == NULL)
      return e_invalid_argument;

   alsa_output* alsa = (alsa_output*)malloc(sizeof(alsa_output));

   if (alsa == NULL)
      return e_internal_error;

   alsa->callback = NULL;
   alsa->user_data = NULL;
   alsa->dev = NULL;
   alsa->pcm_callback = NULL;

   int err = 0;
   printf("ALSA (ver. %s) initialization...\n", SND_LIB_VERSION_STR);

   err = snd_pcm_open(&alsa->dev, device, SND_PCM_STREAM_PLAYBACK, 0);
   check_alsa_error(err);

   (*pctx) = (output_ctx*)alsa;

   printf("ALSA initialized suceessfully\n");

   return e_ok;
}

int output_set_params(output_ctx* ctx, const output_param* param)
{
   if (ctx == NULL)
      return e_invalid_argument;

   if (param == NULL)
      return e_invalid_argument;

   alsa_output* alsa = (alsa_output*)ctx;

   if (alsa == NULL)
      return e_internal_error;

   int err = 0;

   snd_pcm_hw_params_t* hw_params = NULL;
   err = snd_pcm_hw_params_malloc(&hw_params);
   check_alsa_error(err);

   err = snd_pcm_hw_params_any(alsa->dev, hw_params);
   check_alsa_error(err);

   int dir = 0;
   int buffer_time = 500000;
   int period_time = 100000;

   err = snd_pcm_hw_params_set_access(alsa->dev, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
   check_alsa_error(err);

   err = snd_pcm_hw_params_set_format(alsa->dev, hw_params, SND_PCM_FORMAT_U8);
   check_alsa_error(err);

   err = snd_pcm_hw_params_set_channels(alsa->dev, hw_params, param->channels);
   check_alsa_error(err);

   unsigned int rate = param->rate;
   err = snd_pcm_hw_params_set_rate_near(alsa->dev, hw_params, &rate, 0);
   check_alsa_error(err);

   err = snd_pcm_hw_params_set_buffer_time_near(alsa->dev, hw_params, &buffer_time, &dir);
   check_alsa_error(err);

   err = snd_pcm_hw_params_get_buffer_size(hw_params, &alsa->buffer_size);
   check_alsa_error(err);

   err = snd_pcm_hw_params_set_period_time_near(alsa->dev, hw_params, &period_time, &dir);
   check_alsa_error(err);

   err = snd_pcm_hw_params_get_period_size(hw_params, &alsa->period_size, &dir);
   check_alsa_error(err);

   printf("Buffer size:\t%d bytes\nPeriod size:\t%d bytes\n", alsa->buffer_size, alsa->period_size);

   err = snd_pcm_hw_params(alsa->dev, hw_params);
   snd_pcm_hw_params_free(hw_params);
   check_alsa_error(err);

   snd_pcm_sw_params_t* sw_params = NULL;
   err = snd_pcm_sw_params_malloc(&sw_params);
   check_alsa_error(err);

   err = snd_pcm_sw_params_current(alsa->dev, sw_params);
   check_alsa_error(err);

   err = snd_pcm_sw_params_set_start_threshold(alsa->dev, sw_params, (alsa->buffer_size/alsa->period_size)*alsa->period_size);
   check_alsa_error(err);

   err = snd_pcm_sw_params_set_avail_min(alsa->dev, sw_params, alsa->period_size);
   check_alsa_error(err);

   err = snd_pcm_sw_params(alsa->dev, sw_params);
   check_alsa_error(err);

   snd_pcm_sw_params_free(sw_params);

   err = snd_pcm_prepare(alsa->dev);
   check_alsa_error(err);

   printf("ALSA params was set suceessfully\n");

   return e_ok;
}

int output_shutdown(output_ctx* ctx)
{
   if (ctx == NULL)
      return e_invalid_argument;

   alsa_output* alsa = (alsa_output*)ctx;

   if (alsa->dev == NULL)
      return e_invalid_argument;

   int err = snd_pcm_close(alsa->dev);
   check_alsa_error(err);

   alsa->dev = NULL;
   alsa->status = s_not_initialized;

   free(ctx);

   printf("ALSA shutdown success\n");

   return e_ok;
}

void alsa_callback(snd_async_handler_t *pcm_callback)
{
   void *private_data = snd_async_handler_get_callback_private(pcm_callback);
   alsa_output* alsa = (alsa_output*)private_data;

   if (alsa == NULL)
      return;

   if (alsa->dev == NULL)
      return;

   snd_pcm_sframes_t avail = snd_pcm_avail_update(alsa->dev);
   while (avail >= alsa->period_size)
   {
      if (alsa->callback != NULL)
      {
         int res = (*alsa->callback)((output_ctx*)alsa, alsa->period_size, alsa->user_data);
         if (res < 0)
            break;
      }
      avail = snd_pcm_avail_update(alsa->dev);
   }
   if (avail == -EPIPE)
   {
      printf("avail error: %d\n", avail);   
   }
   //printf("callback exit\n");
}

int output_set_callback(output_ctx* ctx, buffer_available_proc callback, void* user_data)
{
   if (ctx == NULL)
      return e_invalid_argument;

   alsa_output* alsa = (alsa_output*)ctx;

   alsa->callback = callback;
   alsa->user_data = user_data;

   int err = 0;

   if (alsa->pcm_callback != NULL)
   {
      printf("Clearing callback..\n");
      err = snd_async_del_handler(alsa->pcm_callback);
      check_alsa_error(err);

      alsa->pcm_callback = NULL;
   }

   if (callback != NULL)
   {
      printf("Setting callback..\n");
      err = snd_async_add_pcm_handler(&alsa->pcm_callback, alsa->dev, alsa_callback, alsa);
      check_alsa_error(err);
   }
 
   return e_ok;
}

int output_write_data(output_ctx* ctx, const void* data, size_t* psize)
{
   if (ctx == NULL)
      return e_invalid_argument;

   if (data == NULL)
      return e_invalid_argument;

   if (psize == NULL)
      return e_invalid_argument;

   size_t size = (*psize);

   if (size == 0)
      return e_invalid_argument;

   alsa_output* alsa = (alsa_output*)ctx;

   //printf("writing %d bytes\n", *psize);
   int res = snd_pcm_writei(alsa->dev, data, *psize);
   if (res < 0)
   {
      res = snd_pcm_recover(alsa->dev, res, 0);
      check_alsa_error_void(res);
   }
   (*psize) = res;

   return e_ok;
}

int output_start(output_ctx* ctx)
{
   if (ctx == NULL)
      return e_invalid_argument;

   alsa_output* alsa = (alsa_output*)ctx;

   if (alsa->dev == NULL)
      return e_invalid_argument;

   int err = 0;
   printf("PCM state: %d\n", snd_pcm_state(alsa->dev));
   switch (snd_pcm_state(alsa->dev))
   {
      case SND_PCM_STATE_PREPARED:
         printf ("Starting..\n");
         err = snd_pcm_start(alsa->dev);
         check_alsa_error(err);

         size_t size = alsa->period_size;
         char* buf = malloc(size);
         memset(buf, 0, size);
         output_write_data(ctx, buf, &size);
         free(buf);
         break;

      default:
         printf("Device is not prepared\n");
         break;
   }
   printf("PCM state: %d\n", snd_pcm_state(alsa->dev));
   return e_ok;
}

int output_stop(output_ctx* ctx)
{
   if (ctx == NULL)
      return e_invalid_argument;

   alsa_output* alsa = (alsa_output*)ctx;

   if (alsa->dev == NULL)
      return e_invalid_argument;

   int err = 0;
   printf("PCM state: %d\n", snd_pcm_state(alsa->dev));
   switch (snd_pcm_state(alsa->dev))
   {
      case SND_PCM_STATE_RUNNING:
      case SND_PCM_STATE_XRUN:
      case SND_PCM_STATE_PAUSED:
      case SND_PCM_STATE_DRAINING:
         printf("Stopping..\n");
         err = snd_pcm_drop(alsa->dev);
         check_alsa_error(err);
         break;

      default:
         printf("Device already stopped\n");
         break;
   }
   printf("PCM state: %d\n", snd_pcm_state(alsa->dev));
   return e_ok;
}


int output_get_status(output_ctx* ctx, output_status* pstatus)
{
   if (ctx == NULL)
      return e_invalid_argument;

   if (pstatus == NULL)
      return e_invalid_argument;

   alsa_output* alsa = (alsa_output*)ctx;
   (*pstatus) = alsa->status;

   return e_ok;
}
/*
int my_callback(output_ctx* ctx, size_t available, void* user_data)
{
   //printf("Callback: %d bytes available\n", available);

   unsigned char buf[16*1024];
   size_t size = sizeof(buf);
   if (size > available)
   {
      size = available;
   }

   size_t r = read(0, buf, size);
   
   if (r > 0)
   {
      size = r;
      output_write_data(ctx, buf, &size);
      return 0;
   }
   else
   {
      eof_ = 1;
      output_stop(ctx);
      printf("EOF\n");
   }
   return -1;
}
int main()
{
   output_ctx* ctx = NULL;
   output_param param = { sizeof(output_param), 0 };
   param.rate = 22050;
   param.channels = 1;

   int err = 0;

   err = output_init(&ctx, "default");
   if (err != e_ok)
      return err;

   err = output_set_params(ctx, &param);
   if (err != e_ok)
      return err;

   err = output_set_callback(ctx, my_callback, NULL);
   if (err != e_ok)
      return err;

   err = output_start(ctx);
   if (err != e_ok)
      return err;

   int a = 0;
   while (a < 1000 && eof_ == 0)
   {
      //printf("Sleeping...\n");
      sleep(1);
      //printf("Sleep ended\n");
      if ((a % 100) == 0)
      {
         printf("sleep #%d\n", a);
      }
      //if (a == 100)
      {
         //break;
      }
      ++a;
   }

   err = output_stop(ctx);

   err = output_shutdown(ctx);
   return 0;
}
*/
